/*
 * The maRla Project - Graphical problem solver for statistical calculations.
 * Copyright © 2011 Cedarville University
 * http://marla.googlecode.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/*
 * @(#)ColorPicker.java  1.0  2008-03-01
 *
 * Copyright (c) 2008 Jeremy Wood
 * E-mail: mickleness@gmail.com
 * All rights reserved.
 *
 * The copyright of this software is owned by Jeremy Wood.
 * You may not use, copy or modify this software, except in
 * accordance with the license agreement you entered into with
 * Jeremy Wood. For details see accompanying license terms.
 */

package marla.ide.gui.colorpicker;

import marla.ide.gui.ViewPanel;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.ButtonGroup;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

/** This is a panel that offers a robust set of controls to pick a color.
 * <P>This was originally intended to replace the <code>JColorChooser</code>.
 * To use this class to create a color choosing dialog, simply call:
 * <BR><code>ColorPicker.showDialog(frame, originalColor);</code>
 * <P>However this panel is also resizable, and it can exist in other contexts.
 * For example, you might try the following panel:
 * <BR><code>ColorPicker picker = new ColorPicker(false, false);</code>
 * <BR><code>picker.setPreferredSize(new Dimension(200,160));</code>
 * <BR><code>picker.setMode(ColorPicker.HUE);</code>
 * <P>This will create a miniature color picker that still lets the user choose
 * from every available color, but it does not include all the buttons and
 * numeric controls on the right side of the panel.  This might be ideal if you
 * are working with limited space, or non-power-users who don't need the
 * RGB values of a color.  The <code>main()</code> method of this class demonstrates
 * possible ways you can customize a <code>ColorPicker</code> component.
 * <P>To listen to color changes to this panel, you can add a <code>PropertyChangeListener</code>
 * listening for changes to the <code>SELECTED_COLOR_PROPERTY</code>.  This will be triggered only
 * when the RGB value of the selected color changes.
 * <P>To listen to opacity changes to this panel, use a <code>PropertyChangeListener</code> listening
 * for changes to the <code>OPACITY_PROPERTY</code>.
 * 
 * @version 1.2
 * @author Jeremy Wood
 */
public final class ColorPicker extends JPanel
{
    private static final long serialVersionUID = 3L;

    public static Color showDialog(Container owner, Color originalColor, ViewPanel viewPanel)
    {
        if (owner instanceof Window)
        {
            return showDialog ((Window) owner, originalColor, viewPanel);
        }
        else
        {
            Logger.getLogger (ColorPicker.class.getName ()).log (Level.SEVERE, "Not a Window subclass: {0}", owner);
            Toolkit.getDefaultToolkit ().beep ();
        }
        return null;
    }

    /** This creates a modal dialog prompting the user to select a color.
     * <P>This uses a generic dialog title: "Choose a Color", and does not include opacity.
     *
     * @param owner the dialog this new dialog belongs to.  This must be a Frame or a Dialog.
     * Java 1.6 supports Windows here, but this package is designed/compiled to work in Java 1.4,
     * so an <code>IllegalArgumentException</code> will be thrown if this component is a <code>Window</code>.
     * @param originalColor the color the <code>ColorPicker</code> initially points to.
     * @return the <code>Color</code> the user chooses, or <code>null</code> if the user cancels the dialog.
     */
    public static Color showDialog(Window owner, Color originalColor, ViewPanel viewPanel)
    {
        return showDialog (owner, null, originalColor, false, viewPanel);
    }

    /** This creates a modal dialog prompting the user to select a color.
     * <P>This uses a generic dialog title: "Choose a Color".
     *
     * @param owner the dialog this new dialog belongs to.  This must be a Frame or a Dialog.
     * Java 1.6 supports Windows here, but this package is designed/compiled to work in Java 1.4,
     * so an <code>IllegalArgumentException</code> will be thrown if this component is a <code>Window</code>.
     * @param originalColor the color the <code>ColorPicker</code> initially points to.
     * @param includeOpacity whether to add a control for the opacity of the color.
     * @return the <code>Color</code> the user chooses, or <code>null</code> if the user cancels the dialog.
     */
    public static Color showDialog(Window owner, Color originalColor, boolean includeOpacity, ViewPanel viewPanel)
    {
        return showDialog (owner, null, originalColor, includeOpacity, viewPanel);
    }

    /** This creates a modal dialog prompting the user to select a color.
     *
     * @param owner the dialog this new dialog belongs to.  This must be a Frame or a Dialog.
     * Java 1.6 supports Windows here, but this package is designed/compiled to work in Java 1.4,
     * so an <code>IllegalArgumentException</code> will be thrown if this component is a <code>Window</code>.
     * @param title the title for the dialog.
     * @param originalColor the color the <code>ColorPicker</code> initially points to.
     * @param includeOpacity whether to add a control for the opacity of the color.
     * @return the <code>Color</code> the user chooses, or <code>null</code> if the user cancels the dialog.
     */
    public static Color showDialog(Window owner, String title, Color originalColor, boolean includeOpacity, ViewPanel viewPanel)
    {
        ColorPickerDialog d;
        if (owner instanceof Frame || owner == null)
        {
            d = new ColorPickerDialog ((Frame) owner, originalColor, includeOpacity, viewPanel);
        }
        else if (owner instanceof Dialog)
        {
            d = new ColorPickerDialog ((Dialog) owner, originalColor, includeOpacity, viewPanel);
        }
        else
        {
            throw new IllegalArgumentException ("the owner (" + owner.getClass ().getName () + ") must be a java.awt.Frame or a java.awt.Dialog");
        }

        hexLabel.setFont (new Font ("Verdana", Font.BOLD, 11));

        d.setTitle (title == null
                ? "Choose a Color"
                : title);
        d.pack ();
        d.setVisible (true);
        return d.getColor ();
    }
    /** <code>PropertyChangeEvents</code> will be triggered for this property when the selected color
     * changes.
     * <P>(Events are only created when then RGB values of the color change.  This means, for example,
     * that the change from HSB(0,0,0) to HSB(.4,0,0) will <i>not</i> generate events, because when the
     * brightness stays zero the RGB color remains (0,0,0).  So although the hue moved around, the color
     * is still black, so no events are created.)
     *
     */
    public static final String SELECTED_COLOR_PROPERTY = "selected color";
    /** <code>PropertyChangeEvents</code> will be triggered for this property when <code>setModeControlsVisible()</code>
     * is called.
     */
    public static final String MODE_CONTROLS_VISIBLE_PROPERTY = "mode controls visible";
    /** <code>PropertyChangeEvents</code> will be triggered when the opacity value is
     * adjusted.
     */
    public static final String OPACITY_PROPERTY = "opacity";
    /** <code>PropertyChangeEvents</code> will be triggered when the mode changes.
     * (That is, when the wheel switches from HUE, SAT, BRI, RED, GREEN, or BLUE modes.)
     */
    public static final String MODE_PROPERTY = "mode";
    /** Used to indicate when we're in "hue mode". */
    protected static final int HUE = 0;
    /** Used to indicate when we're in "brightness mode". */
    protected static final int BRI = 1;
    /** Used to indicate when we're in "saturation mode". */
    protected static final int SAT = 2;
    /** Used to indicate when we're in "red mode". */
    protected static final int RED = 3;
    /** Used to indicate when we're in "green mode". */
    protected static final int GREEN = 4;
    /** Used to indicate when we're in "blue mode". */
    protected static final int BLUE = 5;
    /** The vertical slider */
    private JSlider slider = new JSlider (JSlider.VERTICAL, 0, 100, 0);
    ChangeListener changeListener = new ChangeListener ()
    {
        @Override
        public void stateChanged(ChangeEvent e)
        {
            Object src = e.getSource ();

            if (hue.contains (src) || sat.contains (src) || bri.contains (src))
            {
                if (adjustingSpinners > 0)
                {
                    return;
                }

                setHSB (hue.getFloatValue () / 360f,
                        sat.getFloatValue () / 100f,
                        bri.getFloatValue () / 100f);
            }
            else if (red.contains (src) || green.contains (src) || blue.contains (src))
            {
                if (adjustingSpinners > 0)
                {
                    return;
                }

                setRGB (red.getIntValue (),
                        green.getIntValue (),
                        blue.getIntValue ());
            }
            else if (src == colorPanel)
            {
                if (adjustingColorPanel > 0)
                {
                    return;
                }

                int mode = getMode ();
                if (mode == HUE || mode == BRI || mode == SAT)
                {
                    float[] hsb = colorPanel.getHSB ();
                    setHSB (hsb[0], hsb[1], hsb[2]);
                }
                else
                {
                    int[] rgb = colorPanel.getRGB ();
                    setRGB (rgb[0], rgb[1], rgb[2]);
                }
            }
            else if (src == slider)
            {
                if (adjustingSlider > 0)
                {
                    return;
                }

                int v = slider.getValue ();
                Option option = getSelectedOption ();
                option.setValue (v);
            }
            else if (alpha.contains (src))
            {
                if (adjustingOpacity > 0)
                {
                    return;
                }
                int v = alpha.getIntValue ();
                setOpacity (((float) v) / 255f);
            }
            else if (src == opacitySlider)
            {
                if (adjustingOpacity > 0)
                {
                    return;
                }

                float newValue = (((float) opacitySlider.getValue ()) / 255f);
                setOpacity (newValue);
            }
        }
    };
    ActionListener actionListener = new ActionListener ()
    {
        @Override
        public void actionPerformed(ActionEvent e)
        {
            Object src = e.getSource ();
            if (src == hue.radioButton)
            {
                setMode (HUE);
            }
            else if (src == bri.radioButton)
            {
                setMode (BRI);
            }
            else if (src == sat.radioButton)
            {
                setMode (SAT);
            }
            else if (src == red.radioButton)
            {
                setMode (RED);
            }
            else if (src == green.radioButton)
            {
                setMode (GREEN);
            }
            else if (src == blue.radioButton)
            {
                setMode (BLUE);
            }
        }
    };

    /** @return the currently selected <code>Option</code>
     */
    private Option getSelectedOption()
    {
        int mode = getMode ();
        if (mode == HUE)
        {
            return hue;
        }
        else if (mode == SAT)
        {
            return sat;
        }
        else if (mode == BRI)
        {
            return bri;
        }
        else if (mode == RED)
        {
            return red;
        }
        else if (mode == GREEN)
        {
            return green;
        }
        else
        {
            return blue;
        }
    }

    /** This thread will wait a second or two before committing the text in
     * the hex TextField.  This gives the user a chance to finish typing...
     * but if the user is just waiting for something to happen, this makes sure
     * after a second or two something happens.
     */
    class HexUpdateThread extends Thread
    {
        long myStamp;
        String text;

        public HexUpdateThread(long stamp, String s)
        {
            myStamp = stamp;
            text = s;
        }

        @Override
        public void run()
        {
            if (SwingUtilities.isEventDispatchThread () == false)
            {
                long WAIT = 1500;

                while (System.currentTimeMillis () - myStamp < WAIT)
                {
                    try
                    {
                        long delay = WAIT - (System.currentTimeMillis () - myStamp);
                        if (delay < 1)
                        {
                            delay = 1;
                        }
                        Thread.sleep (delay);
                    }
                    catch (Exception e)
                    {
                        Thread.yield ();
                    }
                }
                SwingUtilities.invokeLater (this);
                return;
            }

            if (myStamp != hexDocListener.lastTimeStamp)
            {
                //another event has come along and trumped this one
                return;
            }

            if (text.length () > 6)
            {
                text = text.substring (0, 6);
            }
            while (text.length () < 6)
            {
                text = text + "0";
            }
            if (hexField.getText ().equals (text))
            {
                return;
            }

            int pos = hexField.getCaretPosition ();
            hexField.setText (text);
            hexField.setCaretPosition (pos);
        }
    }
    HexDocumentListener hexDocListener = new HexDocumentListener ();

    class HexDocumentListener implements DocumentListener
    {
        long lastTimeStamp;

        @Override
        public void changedUpdate(DocumentEvent e)
        {
            lastTimeStamp = System.currentTimeMillis ();

            if (adjustingHexField > 0)
            {
                return;
            }

            String s = hexField.getText ();
            s = stripToHex (s);
            if (s.length () == 6)
            {
                //the user typed 6 digits: we can work with this:
                try
                {
                    int i = Integer.parseInt (s, 16);
                    setRGB (((i >> 16) & 0xff), ((i >> 8) & 0xff), ((i) & 0xff));
                    return;
                }
                catch (NumberFormatException e2)
                {
                }
            }
            Thread thread = new HexUpdateThread (lastTimeStamp, s);
            thread.start ();
            while (System.currentTimeMillis () - lastTimeStamp == 0)
            {
                Thread.yield ();
            }
        }

        /** Strips a string down to only uppercase hex-supported characters. */
        private String stripToHex(String s)
        {
            s = s.toUpperCase ();
            String s2 = "";
            for (int a = 0; a < s.length (); a++)
            {
                char c = s.charAt (a);
                if (c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || c == '5'
                    || c == '6' || c == '7' || c == '8' || c == '9' || c == '0'
                    || c == 'A' || c == 'B' || c == 'C' || c == 'D' || c == 'E' || c == 'F')
                {
                    s2 = s2 + c;
                }
            }
            return s2;
        }

        @Override
        public void insertUpdate(DocumentEvent e)
        {
            changedUpdate (e);
        }

        @Override
        public void removeUpdate(DocumentEvent e)
        {
            changedUpdate (e);
        }
    };
    private Option alpha = new Option ("Alpha:", 255);
    private Option hue = new Option ("Hue:", 360);
    private Option sat = new Option ("Sat:", 100);
    private Option bri = new Option ("Bri:", 100);
    private Option red = new Option ("Red:", 255);
    private Option green = new Option ("Green:", 255);
    private Option blue = new Option ("Blue:", 255);
    private ColorSwatch preview = new ColorSwatch (50);
    private static JLabel hexLabel = new JLabel ("Hex:");
    private JTextField hexField = new JTextField ("000000");
    /** Used to indicate when we're internally adjusting the value of the spinners.
     * If this equals zero, then incoming events are triggered by the user and must be processed.
     * If this is not equal to zero, then incoming events are triggered by another method
     * that's already responding to the user's actions.
     */
    private int adjustingSpinners = 0;
    /** Used to indicate when we're internally adjusting the value of the slider.
     * If this equals zero, then incoming events are triggered by the user and must be processed.
     * If this is not equal to zero, then incoming events are triggered by another method
     * that's already responding to the user's actions.
     */
    private int adjustingSlider = 0;
    /** Used to indicate when we're internally adjusting the selected color of the ColorPanel.
     * If this equals zero, then incoming events are triggered by the user and must be processed.
     * If this is not equal to zero, then incoming events are triggered by another method
     * that's already responding to the user's actions.
     */
    private int adjustingColorPanel = 0;
    /** Used to indicate when we're internally adjusting the value of the hex field.
     * If this equals zero, then incoming events are triggered by the user and must be processed.
     * If this is not equal to zero, then incoming events are triggered by another method
     * that's already responding to the user's actions.
     */
    private int adjustingHexField = 0;
    /** Used to indicate when we're internally adjusting the value of the opacity.
     * If this equals zero, then incoming events are triggered by the user and must be processed.
     * If this is not equal to zero, then incoming events are triggered by another method
     * that's already responding to the user's actions.
     */
    private int adjustingOpacity = 0;
    /** The "expert" controls are the controls on the right side
     * of this panel: the labels/spinners/radio buttons.
     */
    private JPanel expertControls = new JPanel (new GridBagLayout ());
    private ColorPickerPanel colorPanel = new ColorPickerPanel ();
    private JSlider opacitySlider = new JSlider (0, 255, 255);
    private JLabel opacityLabel = new JLabel ("Opacity:");

    /** Create a new <code>ColorPicker</code> with all controls visible except opacity. */
    public ColorPicker()
    {
        this (true, false);
    }

    /** Create a new <code>ColorPicker</code>.
     *
     * @param showExpertControls the labels/spinners/buttons on the right side of a
     * <code>ColorPicker</code> are optional.  This boolean will control whether they
     * are shown or not.
     * <P>It may be that your users will never need or want numeric control when
     * they choose their colors, so hiding this may simplify your interface.
     * @param includeOpacity whether the opacity controls will be shown
     */
    public ColorPicker(boolean showExpertControls, boolean includeOpacity)
    {
        super (new GridBagLayout ());
        GridBagConstraints c = new GridBagConstraints ();

        Insets normalInsets = new Insets (3, 3, 3, 3);

        JPanel options = new JPanel (new GridBagLayout ());
        c.gridx = 0;
        c.gridy = 0;
        c.weightx = 1;
        c.weighty = 1;
        c.insets = normalInsets;
        ButtonGroup bg = new ButtonGroup ();

        //put them in order
        Option[] optionsArray = new Option[]
        {
            hue, sat, bri, red, green, blue
        };

        for (int a = 0; a < optionsArray.length; a++)
        {
            if (a == 3 || a == 6)
            {
                c.insets = new Insets (normalInsets.top + 10, normalInsets.left, normalInsets.bottom, normalInsets.right);
            }
            else
            {
                c.insets = normalInsets;
            }
            c.anchor = GridBagConstraints.EAST;
            c.fill = GridBagConstraints.NONE;
            options.add (optionsArray[a].label, c);
            c.gridx++;
            c.anchor = GridBagConstraints.WEST;
            c.fill = GridBagConstraints.HORIZONTAL;
            if (optionsArray[a].spinner != null)
            {
                options.add (optionsArray[a].spinner, c);
            }
            else
            {
                options.add (optionsArray[a].slider, c);
            }
            c.gridx++;
            c.fill = GridBagConstraints.NONE;
            options.add (optionsArray[a].radioButton, c);
            c.gridy++;
            c.gridx = 0;
            bg.add (optionsArray[a].radioButton);
        }
        c.insets = new Insets (normalInsets.top + 10, normalInsets.left, normalInsets.bottom, normalInsets.right);
        c.anchor = GridBagConstraints.EAST;
        c.fill = GridBagConstraints.NONE;
        options.add (hexLabel, c);
        c.gridx++;
        c.anchor = GridBagConstraints.WEST;
        c.fill = GridBagConstraints.HORIZONTAL;
        options.add (hexField, c);
        c.gridy++;
        c.gridx = 0;
        c.anchor = GridBagConstraints.EAST;
        c.fill = GridBagConstraints.NONE;
        options.add (alpha.label, c);
        c.gridx++;
        c.anchor = GridBagConstraints.WEST;
        c.fill = GridBagConstraints.HORIZONTAL;
        options.add (alpha.spinner, c);

        c.gridx = 0;
        c.gridy = 0;
        c.weightx = 1;
        c.weighty = 1;
        c.fill = GridBagConstraints.BOTH;
        c.anchor = GridBagConstraints.CENTER;
        c.insets = normalInsets;
        c.gridwidth = 2;
        add (colorPanel, c);

        c.gridwidth = 1;
        c.insets = normalInsets;
        c.gridx += 2;
        c.weighty = 1;
        c.gridwidth = 1;
        c.fill = GridBagConstraints.VERTICAL;
        c.weightx = 0;
        add (slider, c);

        c.gridx++;
        c.fill = GridBagConstraints.VERTICAL;
        c.gridheight = GridBagConstraints.REMAINDER;
        c.anchor = GridBagConstraints.CENTER;
        c.insets = new Insets (0, 0, 0, 0);
        add (expertControls, c);

        c.gridx = 0;
        c.gridheight = 1;
        c.gridy = 1;
        c.weightx = 0;
        c.weighty = 0;
        c.insets = normalInsets;
        c.anchor = GridBagConstraints.CENTER;
        add (opacityLabel, c);
        c.gridx++;
        c.gridwidth = 2;
        c.weightx = 1;
        c.fill = GridBagConstraints.HORIZONTAL;
        add (opacitySlider, c);

        c.gridx = 0;
        c.gridy = 0;
        c.gridheight = 1;
        c.gridwidth = 1;
        c.fill = GridBagConstraints.BOTH;
        c.weighty = 1;
        c.anchor = GridBagConstraints.CENTER;
        c.weightx = 1;
        c.insets = new Insets (normalInsets.top, normalInsets.left + 8, normalInsets.bottom + 10, normalInsets.right + 8);
        expertControls.add (preview, c);
        c.gridy++;
        c.weighty = 0;
        c.anchor = GridBagConstraints.CENTER;
        c.insets = new Insets (normalInsets.top, normalInsets.left, 0, normalInsets.right);
        expertControls.add (options, c);

        preview.setOpaque (true);
        colorPanel.setPreferredSize (new Dimension (expertControls.getPreferredSize ().height,
                                                    expertControls.getPreferredSize ().height));

        slider.addChangeListener (changeListener);
        colorPanel.addChangeListener (changeListener);
        slider.setUI (new ColorPickerSliderUI (slider, this));
        hexField.getDocument ().addDocumentListener (hexDocListener);
        setMode (BRI);

        setExpertControlsVisible (showExpertControls);

        setOpacityVisible (includeOpacity);

        opacitySlider.addChangeListener (changeListener);

        setOpacity (1);
    }

    /** This controls whether the hex field (and label) are visible or not.
     * <P>Note this lives inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
     * has been called, then calling this method makes no difference: the hex controls will be hidden.
     */
    public void setHexControlsVisible(boolean b)
    {
        hexLabel.setVisible (b);
        hexField.setVisible (b);
    }

    /** This controls whether the preview swatch visible or not.
     * <P>Note this lives inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
     * has been called, then calling this method makes no difference: the swatch will be hidden.
     */
    public void setPreviewSwatchVisible(boolean b)
    {
        preview.setVisible (b);
    }

    /** The labels/spinners/buttons on the right side of a <code>ColorPicker</code>
     * are optional.  This method will control whether they are shown or not.
     * <P>It may be that your users will never need or want numeric control when
     * they choose their colors, so hiding this may simplify your interface.
     *
     * @param b whether to show or hide the expert controls.
     */
    public void setExpertControlsVisible(boolean b)
    {
        expertControls.setVisible (b);
    }

    /** @return the current HSB coordinates of this <code>ColorPicker</code>.
     * Each value is between [0,1].
     *
     */
    public float[] getHSB()
    {
        return new float[]
                {
                    hue.getFloatValue () / 360f,
                    sat.getFloatValue () / 100f,
                    bri.getFloatValue () / 100f
                };
    }

    /** @return the current RGB coordinates of this <code>ColorPicker</code>.
     * Each value is between [0,255].
     *
     */
    public int[] getRGB()
    {
        return new int[]
                {
                    red.getIntValue (),
                    green.getIntValue (),
                    blue.getIntValue ()
                };
    }

    /** Returns the currently selected opacity (a float between 0 and 1).
     *
     * @return the currently selected opacity (a float between 0 and 1).
     */
    public float getOpacity()
    {
        return ((float) opacitySlider.getValue ()) / 255f;
    }
    private float lastOpacity = 1;

    /** Sets the currently selected opacity.
     *
     * @param v a float between 0 and 1.
     */
    public void setOpacity(float v)
    {
        if (v < 0 || v > 1)
        {
            throw new IllegalArgumentException ("The opacity (" + v + ") must be between 0 and 1.");
        }
        adjustingOpacity++;
        try
        {
            int i = (int) (255 * v);
            opacitySlider.setValue (i);
            alpha.spinner.setValue (new Integer (i));
            if (lastOpacity != v)
            {
                firePropertyChange (OPACITY_PROPERTY, new Float (lastOpacity), new Float (i));
                Color c = preview.getForeground ();
                preview.setForeground (new Color (c.getRed (), c.getGreen (), c.getBlue (), i));
            }
            lastOpacity = v;
        }
        finally
        {
            adjustingOpacity--;
        }
    }

    /** Sets the mode of this <code>ColorPicker</code>.
     * This is especially useful if this picker is in non-expert mode, so
     * the radio buttons are not visible for the user to directly select.
     *
     * @param mode must be HUE, SAT, BRI, RED, GREEN or BLUE.
     */
    public void setMode(int mode)
    {
        if (!(mode == HUE || mode == SAT || mode == BRI || mode == RED || mode == GREEN || mode == BLUE))
        {
            throw new IllegalArgumentException ("mode must be HUE, SAT, BRI, REd, GREEN, or BLUE");
        }
        putClientProperty (MODE_PROPERTY, new Integer (mode));
        hue.radioButton.setSelected (mode == HUE);
        sat.radioButton.setSelected (mode == SAT);
        bri.radioButton.setSelected (mode == BRI);
        red.radioButton.setSelected (mode == RED);
        green.radioButton.setSelected (mode == GREEN);
        blue.radioButton.setSelected (mode == BLUE);

        colorPanel.setMode (mode);
        adjustingSlider++;
        try
        {
            slider.setValue (0);
            Option option = getSelectedOption ();
            slider.setInverted (mode == HUE);
            int max = option.getMaximum ();
            slider.setMaximum (max);
            slider.setValue (option.getIntValue ());
            slider.repaint ();

            if (mode == HUE || mode == SAT || mode == BRI)
            {
                setHSB (hue.getFloatValue () / 360f,
                        sat.getFloatValue () / 100f,
                        bri.getFloatValue () / 100f);
            }
            else
            {
                setRGB (red.getIntValue (),
                        green.getIntValue (),
                        blue.getIntValue ());

            }
        }
        finally
        {
            adjustingSlider--;
        }
    }

    /** This controls whether the radio buttons that adjust the mode are visible.
     * <P>(These buttons appear next to the spinners in the expert controls.)
     * <P>Note these live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
     * has been called, then these will never be visible.
     *
     * @param b
     */
    public void setModeControlsVisible(boolean b)
    {
        hue.radioButton.setVisible (b && hue.isVisible ());
        sat.radioButton.setVisible (b && sat.isVisible ());
        bri.radioButton.setVisible (b && bri.isVisible ());
        red.radioButton.setVisible (b && red.isVisible ());
        green.radioButton.setVisible (b && green.isVisible ());
        blue.radioButton.setVisible (b && blue.isVisible ());
        putClientProperty (MODE_CONTROLS_VISIBLE_PROPERTY, b);
    }

    /** @return the current mode of this <code>ColorPicker</code>.
     * <BR>This will return <code>HUE</code>,  <code>SAT</code>,  <code>BRI</code>,
     * <code>RED</code>,  <code>GREEN</code>, or <code>BLUE</code>.
     * <P>The default mode is <code>BRI</code>, because that provides the most
     * aesthetic/recognizable color wheel.
     */
    public int getMode()
    {
        Integer i = (Integer) getClientProperty (MODE_PROPERTY);
        if (i == null)
        {
            return -1;
        }
        return i.intValue ();
    }

    /** Sets the current color of this <code>ColorPicker</code>.
     * This method simply calls <code>setRGB()</code> and <code>setOpacity()</code>.
     * @param c the new color to use.
     */
    public void setColor(Color c)
    {
        setRGB (c.getRed (), c.getGreen (), c.getBlue ());
        float opacity = ((float) c.getAlpha ()) / 255f;
        setOpacity (opacity);
    }

    /** Sets the current color of this <code>ColorPicker</code>
     *
     * @param r the red value.  Must be between [0,255].
     * @param g the green value.  Must be between [0,255].
     * @param b the blue value.  Must be between [0,255].
     */
    public void setRGB(int r, int g, int b)
    {
        if (r < 0 || r > 255)
        {
            throw new IllegalArgumentException ("The red value (" + r + ") must be between [0,255].");
        }
        if (g < 0 || g > 255)
        {
            throw new IllegalArgumentException ("The green value (" + g + ") must be between [0,255].");
        }
        if (b < 0 || b > 255)
        {
            throw new IllegalArgumentException ("The blue value (" + b + ") must be between [0,255].");
        }

        Color lastColor = getColor ();

        boolean updateRGBSpinners = adjustingSpinners == 0;

        adjustingSpinners++;
        adjustingColorPanel++;
        int myAlpha = this.alpha.getIntValue ();
        try
        {
            if (updateRGBSpinners)
            {
                red.setValue (r);
                green.setValue (g);
                blue.setValue (b);
            }
            preview.setForeground (new Color (r, g, b, myAlpha));
            float[] hsb = new float[3];
            Color.RGBtoHSB (r, g, b, hsb);
            hue.setValue ((int) (hsb[0] * 360f + .49f));
            sat.setValue ((int) (hsb[1] * 100f + .49f));
            bri.setValue ((int) (hsb[2] * 100f + .49f));
            colorPanel.setRGB (r, g, b);
            updateHexField ();
            updateSlider ();
        }
        finally
        {
            adjustingSpinners--;
            adjustingColorPanel--;
        }
        Color newColor = getColor ();
        if (lastColor.equals (newColor) == false)
        {
            firePropertyChange (SELECTED_COLOR_PROPERTY, lastColor, newColor);
        }
    }

    /** @return the current <code>Color</code> this <code>ColorPicker</code> has selected.
     * <P>This is equivalent to:
     * <BR><code>int[] i = getRGB();</code>
     * <BR><code>return new Color(i[0], i[1], i[2], opacitySlider.getValue());</code>
     */
    public Color getColor()
    {
        int[] i = getRGB ();
        return new Color (i[0], i[1], i[2], opacitySlider.getValue ());
    }

    private void updateSlider()
    {
        adjustingSlider++;
        try
        {
            int mode = getMode ();
            if (mode == HUE)
            {
                slider.setValue (hue.getIntValue ());
            }
            else if (mode == SAT)
            {
                slider.setValue (sat.getIntValue ());
            }
            else if (mode == BRI)
            {
                slider.setValue (bri.getIntValue ());
            }
            else if (mode == RED)
            {
                slider.setValue (red.getIntValue ());
            }
            else if (mode == GREEN)
            {
                slider.setValue (green.getIntValue ());
            }
            else if (mode == BLUE)
            {
                slider.setValue (blue.getIntValue ());
            }
        }
        finally
        {
            adjustingSlider--;
        }
        slider.repaint ();
    }

    /** This returns the panel with several rows of spinner controls.
     * <P>Note you can also call methods such as <code>setRGBControlsVisible()</code> to adjust
     * which controls are showing.
     * <P>(This returns the panel this <code>ColorPicker</code> uses, so if you put it in
     * another container, it will be removed from this <code>ColorPicker</code>.)
     * @return the panel with several rows of spinner controls.
     */
    public JPanel getExpertControls()
    {
        return expertControls;
    }

    /** This shows or hides the RGB spinner controls.
     * <P>Note these live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
     * has been called, then calling this method makes no difference: the RGB controls will be hidden.
     *
     * @param b whether the controls should be visible or not.
     */
    public void setRGBControlsVisible(boolean b)
    {
        red.setVisible (b);
        green.setVisible (b);
        blue.setVisible (b);
    }

    /** This shows or hides the HSB spinner controls.
     * <P>Note these live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
     * has been called, then calling this method makes no difference: the HSB controls will be hidden.
     *
     * @param b whether the controls should be visible or not.
     */
    public void setHSBControlsVisible(boolean b)
    {
        hue.setVisible (b);
        sat.setVisible (b);
        bri.setVisible (b);
    }

    /** This shows or hides the alpha controls.
     * <P>Note the alpha spinner live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
     * has been called, then this method does not affect that spinner.
     * However, the opacity slider is <i>not</i> affected by the visibility of the export controls.
     * @param b
     */
    public void setOpacityVisible(boolean b)
    {
        opacityLabel.setVisible (b);
        opacitySlider.setVisible (b);
        alpha.label.setVisible (b);
        alpha.spinner.setVisible (b);
    }

    /** @return the <code>ColorPickerPanel</code> this <code>ColorPicker</code> displays. */
    public ColorPickerPanel getColorPanel()
    {
        return colorPanel;
    }

    /** Sets the current color of this <code>ColorPicker</code>
     *
     * @param h the hue value.
     * @param s the saturation value.  Must be between [0,1].
     * @param b the blue value.  Must be between [0,1].
     */
    public void setHSB(float h, float s, float b)
    {
        if (Float.isInfinite (h) || Float.isNaN (h))
        {
            throw new IllegalArgumentException ("The hue value (" + h + ") is not a valid number.");
        }
        //hue is cyclic, so it can be any value:
        while (h < 0)
        {
            h++;
        }
        while (h > 1)
        {
            h--;
        }

        if (s < 0 || s > 1)
        {
            throw new IllegalArgumentException ("The saturation value (" + s + ") must be between [0,1]");
        }
        if (b < 0 || b > 1)
        {
            throw new IllegalArgumentException ("The brightness value (" + b + ") must be between [0,1]");
        }

        Color lastColor = getColor ();

        boolean updateHSBSpinners = adjustingSpinners == 0;
        adjustingSpinners++;
        adjustingColorPanel++;
        try
        {
            if (updateHSBSpinners)
            {
                hue.setValue ((int) (h * 360f + .49f));
                sat.setValue ((int) (s * 100f + .49f));
                bri.setValue ((int) (b * 100f + .49f));
            }

            Color c = new Color (Color.HSBtoRGB (h, s, b));
            int myAlpha = this.alpha.getIntValue ();
            c = new Color (c.getRed (), c.getGreen (), c.getBlue (), myAlpha);
            preview.setForeground (c);
            red.setValue (c.getRed ());
            green.setValue (c.getGreen ());
            blue.setValue (c.getBlue ());
            colorPanel.setHSB (h, s, b);
            updateHexField ();
            updateSlider ();
            slider.repaint ();
        }
        finally
        {
            adjustingSpinners--;
            adjustingColorPanel--;
        }
        Color newColor = getColor ();
        if (lastColor.equals (newColor) == false)
        {
            firePropertyChange (SELECTED_COLOR_PROPERTY, lastColor, newColor);
        }
    }

    private void updateHexField()
    {
        adjustingHexField++;
        try
        {
            int r = red.getIntValue ();
            int g = green.getIntValue ();
            int b = blue.getIntValue ();

            int i = (r << 16) + (g << 8) + b;
            String s = Integer.toHexString (i).toUpperCase ();
            while (s.length () < 6)
            {
                s = "0" + s;
            }
            if (hexField.getText ().equalsIgnoreCase (s) == false)
            {
                hexField.setText (s);
            }
        }
        finally
        {
            adjustingHexField--;
        }
    }

    class Option
    {
        JRadioButton radioButton = new JRadioButton ();
        JSpinner spinner;
        JSlider slider;
        JLabel label;

        public Option(String text, int max)
        {
            spinner = new JSpinner (new SpinnerNumberModel (0, 0, max, 5));
            spinner.addChangeListener (changeListener);

            /*this tries out Tim Boudreaux's new slider UI.
             * It's a good UI, but I think for the ColorPicker
             * the numeric controls are more useful.
             * That is: users who want click-and-drag control to choose
             * their colors don't need any of these Option objects
             * at all; only power users who may have specific RGB
             * values in mind will use these controls: and when they do
             * limiting them to a slider is unnecessary.
             * That's my current position... of course it may
             * not be true in the real world... :)
             */
            //slider = new JSlider(0,max);
            //slider.addChangeListener(changeListener);
            //slider.setUI(new org.netbeans.paint.api.components.PopupSliderUI());

            label = new JLabel (text);
            label.setFont (new Font ("Verdana", Font.BOLD, 11));
            radioButton.addActionListener (actionListener);
        }

        public void setValue(int i)
        {
            if (slider != null)
            {
                slider.setValue (i);
            }
            if (spinner != null)
            {
                spinner.setValue (new Integer (i));
            }
        }

        public int getMaximum()
        {
            if (slider != null)
            {
                return slider.getMaximum ();
            }
            return ((Number) ((SpinnerNumberModel) spinner.getModel ()).getMaximum ()).intValue ();
        }

        public boolean contains(Object src)
        {
            return (src == slider || src == spinner || src == radioButton || src == label);
        }

        public float getFloatValue()
        {
            return getIntValue ();
        }

        public int getIntValue()
        {
            if (slider != null)
            {
                return slider.getValue ();
            }
            return ((Number) spinner.getValue ()).intValue ();
        }

        public boolean isVisible()
        {
            return label.isVisible ();
        }

        public void setVisible(boolean b)
        {
            boolean radioButtonsAllowed = true;
            Boolean z = (Boolean) getClientProperty (MODE_CONTROLS_VISIBLE_PROPERTY);
            if (z != null)
            {
                radioButtonsAllowed = z.booleanValue ();
            }

            radioButton.setVisible (b && radioButtonsAllowed);
            if (slider != null)
            {
                slider.setVisible (b);
            }
            if (spinner != null)
            {
                spinner.setVisible (b);
            }
            label.setVisible (b);
        }
    }
}
